#include "meshGen.hpp"
#include "meshTypeFunctions.hpp"
#include "meshImportExport.hpp"
#include "signalCodes.hpp"
#include "typeHelpers.hpp"
#include <cmath>
#include <cassert>

namespace hms
{

namespace internal
{

template<typename MeshType>
MeshType generate( const MeshGenSettings& s ){
	Index
		nx {s.nCellsX},
		ny {s.nCellsY};
	scalar
		dx {s.dX},
		dy {s.dY};

	if (s.useCellSize && !s.useCellCount){
		auto cellCount = [](scalar meshSize, scalar cellSize) -> Index {
			return static_cast<Index>( std::round( meshSize / cellSize ) );
		};
		nx = cellCount( s.sizeX, s.dX );
		ny = cellCount( s.sizeY, s.dY );
	} else if (!s.useCellSize && s.useCellCount){
		dx = s.sizeX / s.nCellsX;
		dy = s.sizeY / s.nCellsY;
	} else if (!s.useCellSize && !s.useCellCount){
		throw MeshError( "Insufficient data to generate mesh.");
	}
	return uniMeshAs<MeshType>(
		nx,
		ny,
		dx,
		dy,
		s.addBoundaryPatches ? s.useStandardBoundaryPatches : false,
		s.originX,
		s.originY
	);
}

} // namespace internal

MeshVariant generateMesh( const MeshGenSettings& settings ){
	using internal::generate;

	MeshVariant meshVar;
	
	using M = MeshGenSettings::MeshType;
	switch (settings.meshType){
		case M::Mesh:
			meshVar = generate<Mesh>(settings);
			break;
		case M::UniMesh:
			meshVar = generate<UniMesh>(settings);
			break;
		case M::RectMesh:
			meshVar = generate<RectMesh>(settings);
			break;
		case M::StructMesh:
			meshVar = generate<StructMesh>(settings);
			break;
		default:
			UNREACHABLE;
	}

	std::visit( [&]( auto& mesh){
		using MeshType = remove_qualifiers<decltype(mesh)>;
		Array3Xi edgesAtBoundaryVerts;
		if constexpr ( std::is_same_v<MeshType, Mesh> ){
			edgesAtBoundaryVerts = edgesAtBoundaryVertices(
				mesh.edges().vertices(), mesh.nInnerEdges()
			);
		}
		Array3i verts;

		for ( const auto& patch : settings.boundaryPatches ){
			verts = findPatchVertices(mesh, patch);
			addBoundaryPatch( mesh, patch.name, verts, edgesAtBoundaryVerts );
		}
	}, meshVar);

	return meshVar;
}

template<typename MeshType>
Array3i findPatchVertices(
	const MeshBase<MeshType>& meshBase,
	const MeshGenSettings::PatchSettings& patch
){
	const MeshType& mesh { meshBase.derived() };
	auto coordsToIndex = [&]( const Vector2s& coords ) -> Index {
		if constexpr ( std::is_same_v<MeshType, UniMesh> ){
			Array2s ij { ( coords - mesh.origin() ).array() / mesh.diff() };
			return static_cast<Index>(
				std::round( ij.x() ) + (mesh.nCellsX()+1)*std::round( ij.y() )
			);
		} else {
			throw std::logic_error(
				"findPatchVertices: Only UniMesh is implemented."
			);
		}
	};

	/* find beg and end vertex indices */
	Index
		beg { coordsToIndex(patch.beg) },
		end { coordsToIndex(patch.end) },
		next {signalCode::uninitialised};

	/* check whether beg and end vertex are on boundary */
	auto checkIsBoundary = [&](
		Index vert,
		const Vector2s coords,
		std::string_view msg
	){
		try { 
			if constexpr (std::is_base_of_v<StructMeshBase<MeshType>, MeshType>)
				whereIsVertex( vert, mesh.size() );
			else
				throw std::logic_error(
					"findPatchVertices: Only StructMeshBase types are implemented."
				);
		}
		catch( const std::runtime_error& ){
			throw MeshError(fmt::format(
				"(Mesh generator) Boundary vertex coordinates not on boundary!"
				"\nPatch \"{}\", "
				"{} point at coordinates ({}), "
				"vertex index {}",
				patch.name,
				msg,
				coords,
				vert
			));
		}
	};
	checkIsBoundary(beg, patch.beg, "starting");
	checkIsBoundary(end, patch.end, "end");
	
	if constexpr ( std::is_base_of_v<StructMeshBase<MeshType>, MeshType> )
		next = nextBoundaryVertex( beg, patch.clockwise, mesh.size() );
	else
		throw std::logic_error(
			"findPatchVertices: Only StructMeshBase types are implemented."
		);

	return {beg, next, end};
}

} // namespace hms
